Skip to content

Patch TanStack so server functions stop loading the router - #33

Merged
ThinkingSpade merged 2 commits into
mainfrom
perf/patch-tanstack-router-entry
Jul 31, 2026
Merged

Patch TanStack so server functions stop loading the router#33
ThinkingSpade merged 2 commits into
mainfrom
perf/patch-tanstack-router-entry

Conversation

@ThinkingSpade

Copy link
Copy Markdown
Owner

Patch TanStack so server functions stop loading the router

This is a patch to a third-party package. That is a real maintenance cost, so
here is the measurement that justifies it and the exact reasoning.

WHY

createStartHandler's loadEntries() did:

const [routerEntry, startEntry, pluginAdapters] = await Promise.all([
  import("#tanstack-router-entry"),
  import("#tanstack-start-entry"),
  import("#tanstack-start-plugin-adapters")
]);

That runs before the handler knows whether the request is a page render or a
/_serverFn/* call. Importing the router entry pulls the generated route tree
and every route definition -- a 735 KB chunk in this app -- and a server
function never calls getRouter().

wrangler tail on production 2026-07-31, after the previous two commits:

warm invocations   cpuTime 3-8ms    wallTime 4-9ms
cold invocation    cpuTime 48ms     wallTime 1652ms

So a cold isolate burns ~1650 ms of wall time INSIDE the handler against 48 ms
of CPU. That is module loading at request time, and loadEntries() is what
does it. Warm, this Worker answers in 7 ms -- it is not slow, it is repeatedly
re-loading code.

This also explains a null result worth recording: the previous commit removed
232 KB from the EAGER chunk and moved the cold number not at all
(1.7-3.0s before and after). The two are different budgets:

Only the second one is what a user waits for on a cold hit, and the router
chunk is the largest item in it.

THE PATCH

routerEntry had exactly one consumer -- the getRouter() closure -- and that
closure is already async and already memoises via its own router variable.
Moving the import into it changes no behaviour: page renders, server routes and
unresolved router redirects still load the router on first use, just not
server-function calls.

VERIFIED

  • The patched package is installed and correct: loadEntries no longer imports
    #tanstack-router-entry; getRouter does.
  • The prerender step still renders "/" and emits the 5,979-byte SPA shell,
    which exercises the page-render path for real rather than by inspection.
  • pnpm ci:check clean; 2,129 tests passing across 224 files.

NOT YET MEASURED: the wall-clock effect. That needs a deploy and a re-run of
the probe ladder. The prediction being tested is that in-handler cold wallTime
drops well below 1652 ms.

MAINTENANCE NOTE

patches/@tanstack__start-server-core@1.169.15.patch is pinned to that exact
version. A TanStack upgrade will fail to apply it, loudly, which is the
behaviour we want -- re-check that loadEntries still has a single-consumer
routerEntry before re-cutting it.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

This is a patch to a third-party package. That is a real maintenance cost, so
here is the measurement that justifies it and the exact reasoning.

WHY

`createStartHandler`'s `loadEntries()` did:

    const [routerEntry, startEntry, pluginAdapters] = await Promise.all([
      import("#tanstack-router-entry"),
      import("#tanstack-start-entry"),
      import("#tanstack-start-plugin-adapters")
    ]);

That runs before the handler knows whether the request is a page render or a
`/_serverFn/*` call. Importing the router entry pulls the generated route tree
and every route definition -- a 735 KB chunk in this app -- and a server
function never calls `getRouter()`.

`wrangler tail` on production 2026-07-31, after the previous two commits:

    warm invocations   cpuTime 3-8ms    wallTime 4-9ms
    cold invocation    cpuTime 48ms     wallTime 1652ms

So a cold isolate burns ~1650 ms of wall time INSIDE the handler against 48 ms
of CPU. That is module loading at request time, and `loadEntries()` is what
does it. Warm, this Worker answers in 7 ms -- it is not slow, it is repeatedly
re-loading code.

This also explains a null result worth recording: the previous commit removed
232 KB from the EAGER chunk and moved the cold number not at all
(1.7-3.0s before and after). The two are different budgets:

  * eager chunk size  -> heap -> whether the isolate SURVIVES (PR #31 crossed
    that threshold; that was the 130x warm win)
  * chunks loaded at REQUEST time -> wall time on a cold isolate

Only the second one is what a user waits for on a cold hit, and the router
chunk is the largest item in it.

THE PATCH

`routerEntry` had exactly one consumer -- the `getRouter()` closure -- and that
closure is already async and already memoises via its own `router` variable.
Moving the import into it changes no behaviour: page renders, server routes and
unresolved router redirects still load the router on first use, just not
server-function calls.

VERIFIED

- The patched package is installed and correct: `loadEntries` no longer imports
  `#tanstack-router-entry`; `getRouter` does.
- The prerender step still renders "/" and emits the 5,979-byte SPA shell,
  which exercises the page-render path for real rather than by inspection.
- pnpm ci:check clean; 2,129 tests passing across 224 files.

NOT YET MEASURED: the wall-clock effect. That needs a deploy and a re-run of
the probe ladder. The prediction being tested is that in-handler cold wallTime
drops well below 1652 ms.

MAINTENANCE NOTE

`patches/@tanstack__start-server-core@1.169.15.patch` is pinned to that exact
version. A TanStack upgrade will fail to apply it, loudly, which is the
behaviour we want -- re-check that `loadEntries` still has a single-consumer
`routerEntry` before re-cutting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
flyrocketseo c6fd256 Jul 31 2026, 04:28 PM

pnpm-workspace.yaml now declares patchedDependencies, and pnpm install hashes
the patch file to check it against the lockfile. Dockerfile.selfhost copied
only the manifests before running `pnpm install --frozen-lockfile`, so the
install failed with ENOENT on the patch before any source was copied.

Caught by the docker-build CI job on this branch, not guessed at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ThinkingSpade
ThinkingSpade merged commit 4153a12 into main Jul 31, 2026
3 checks passed
@ThinkingSpade
ThinkingSpade deleted the perf/patch-tanstack-router-entry branch July 31, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant